﻿using System;

namespace Util.Conversion {

    #region Public Enums

    /// <summary>
    /// Represents the byte order.
    /// </summary>
    public enum ByteOrder {
        System = 0,
        LittleEndian = 1,
        BigEndian = 2,
    }

    #endregion

    /// <summary>
    /// An endian-aware converter for converting between base data types 
    /// and an array of bytes.
    /// </summary>
    public class BitConverterEx {
        #region Member Variables

        private readonly ByteOrder mFrom;
        private readonly ByteOrder mTo;

        #endregion

        #region Constructors

        public BitConverterEx(ByteOrder from, ByteOrder to) {
            mFrom = from;
            mTo = to;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Indicates the byte order in which data is stored in this platform.
        /// </summary>
        public static ByteOrder SystemByteOrder {
            get { return (BitConverter.IsLittleEndian ? ByteOrder.LittleEndian : ByteOrder.BigEndian); }
        }

        #endregion

        #region Predefined Values

        /// <summary>
        /// Returns a bit converter that converts between little-endian and system byte-order.
        /// </summary>
        public static BitConverterEx LittleEndian {
            get { return new BitConverterEx(ByteOrder.LittleEndian, ByteOrder.System); }
        }

        /// <summary>
        /// Returns a bit converter that converts between big-endian and system byte-order.
        /// </summary>
        public static BitConverterEx BigEndian {
            get { return new BitConverterEx(ByteOrder.BigEndian, ByteOrder.System); }
        }

        /// <summary>
        /// Returns a bit converter that does not do any byte-order conversion.
        /// </summary>
        public static BitConverterEx SystemEndian {
            get { return new BitConverterEx(ByteOrder.System, ByteOrder.System); }
        }

        #endregion

        #region Static Methods

        /// <summary>
        /// Converts the given array of bytes to a Unicode character.
        /// </summary>
        public static char ToChar(byte[] value, int startIndex, ByteOrder from, ByteOrder to) {
            byte[] data = CheckData(value, startIndex, 2, from, to);
            return BitConverter.ToChar(data, 0);
        }

        /// <summary>
        /// Converts the given array of bytes to a 16-bit unsigned integer.
        /// </summary>
        public static ushort ToUInt16(byte[] value, int startIndex, ByteOrder from, ByteOrder to) {
            byte[] data = CheckData(value, startIndex, 2, from, to);
            return BitConverter.ToUInt16(data, 0);
        }

        /// <summary>
        /// Converts the given array of bytes to a 32-bit unsigned integer.
        /// </summary>
        public static uint ToUInt32(byte[] value, int startIndex, ByteOrder from, ByteOrder to) {
            byte[] data = CheckData(value, startIndex, 4, from, to);
            return BitConverter.ToUInt32(data, 0);
        }

        /// <summary>
        /// Converts the given array of bytes to a 64-bit unsigned integer.
        /// </summary>
        public static ulong ToUInt64(byte[] value, int startIndex, ByteOrder from, ByteOrder to) {
            byte[] data = CheckData(value, startIndex, 8, from, to);
            return BitConverter.ToUInt64(data, 0);
        }

        /// <summary>
        /// Converts the given array of bytes to a 16-bit signed integer.
        /// </summary>
        public static short ToInt16(byte[] value, int startIndex, ByteOrder from, ByteOrder to) {
            byte[] data = CheckData(value, startIndex, 2, from, to);
            return BitConverter.ToInt16(data, 0);
        }

        /// <summary>
        /// Converts the given array of bytes to a 32-bit signed integer.
        /// </summary>
        public static int ToInt32(byte[] value, int startIndex, ByteOrder from, ByteOrder to) {
            byte[] data = CheckData(value, startIndex, 4, from, to);
            return BitConverter.ToInt32(data, 0);
        }

        /// <summary>
        /// Converts the given array of bytes to a 64-bit signed integer.
        /// </summary>
        public static long ToInt64(byte[] value, int startIndex, ByteOrder from, ByteOrder to) {
            byte[] data = CheckData(value, startIndex, 8, from, to);
            return BitConverter.ToInt64(data, 0);
        }

        /// <summary>
        /// Converts the given array of bytes to a single precision floating number.
        /// </summary>
        public static float ToSingle(byte[] value, int startIndex, ByteOrder from, ByteOrder to) {
            byte[] data = CheckData(value, startIndex, 4, from, to);
            return BitConverter.ToSingle(data, 0);
        }

        /// <summary>
        /// Converts the given array of bytes to a double precision floating number.
        /// </summary>
        public static double ToDouble(byte[] value, int startIndex, ByteOrder from, ByteOrder to) {
            byte[] data = CheckData(value, startIndex, 8, from, to);
            return BitConverter.ToDouble(data, 0);
        }

        /// <summary>
        /// Converts the given 16-bit unsigned integer to an array of bytes.
        /// </summary>
        public static byte[] GetBytes(ushort value, ByteOrder from, ByteOrder to) {
            byte[] data = BitConverter.GetBytes(value);
            data = CheckData(data, from, to);
            return data;
        }

        /// <summary>
        /// Converts the given 32-bit unsigned integer to an array of bytes.
        /// </summary>
        public static byte[] GetBytes(uint value, ByteOrder from, ByteOrder to) {
            byte[] data = BitConverter.GetBytes(value);
            data = CheckData(data, from, to);
            return data;
        }

        /// <summary>
        /// Converts the given 64-bit unsigned integer to an array of bytes.
        /// </summary>
        public static byte[] GetBytes(ulong value, ByteOrder from, ByteOrder to) {
            byte[] data = BitConverter.GetBytes(value);
            data = CheckData(data, from, to);
            return data;
        }

        /// <summary>
        /// Converts the given 16-bit signed integer to an array of bytes.
        /// </summary>
        public static byte[] GetBytes(short value, ByteOrder from, ByteOrder to) {
            byte[] data = BitConverter.GetBytes(value);
            data = CheckData(data, from, to);
            return data;
        }

        /// <summary>
        /// Converts the given 32-bit signed integer to an array of bytes.
        /// </summary>
        public static byte[] GetBytes(int value, ByteOrder from, ByteOrder to) {
            byte[] data = BitConverter.GetBytes(value);
            data = CheckData(data, from, to);
            return data;
        }

        /// <summary>
        /// Converts the given 64-bit signed integer to an array of bytes.
        /// </summary>
        public static byte[] GetBytes(long value, ByteOrder from, ByteOrder to) {
            byte[] data = BitConverter.GetBytes(value);
            data = CheckData(data, from, to);
            return data;
        }

        /// <summary>
        /// Converts the given single precision floating-point number to an array of bytes.
        /// </summary>
        public static byte[] GetBytes(float value, ByteOrder from, ByteOrder to) {
            byte[] data = BitConverter.GetBytes(value);
            data = CheckData(data, from, to);
            return data;
        }

        /// <summary>
        /// Converts the given double precision floating-point number to an array of bytes.
        /// </summary>
        public static byte[] GetBytes(double value, ByteOrder from, ByteOrder to) {
            byte[] data = BitConverter.GetBytes(value);
            data = CheckData(data, from, to);
            return data;
        }

        #endregion

        #region Instance Methods

        /// <summary>
        /// Converts the given array of bytes to a 16-bit unsigned integer.
        /// </summary>
        public char ToChar(byte[] value, int startIndex) {
            return ToChar(value, startIndex, mFrom, mTo);
        }

        /// <summary>
        /// Converts the given array of bytes to a 16-bit unsigned integer.
        /// </summary>
        public ushort ToUInt16(byte[] value, int startIndex) {
            return ToUInt16(value, startIndex, mFrom, mTo);
        }

        /// <summary>
        /// Converts the given array of bytes to a 32-bit unsigned integer.
        /// </summary>
        public uint ToUInt32(byte[] value, int startIndex) {
            return ToUInt32(value, startIndex, mFrom, mTo);
        }

        /// <summary>
        /// Converts the given array of bytes to a 64-bit unsigned integer.
        /// </summary>
        public ulong ToUInt64(byte[] value, int startIndex) {
            return ToUInt64(value, startIndex, mFrom, mTo);
        }

        /// <summary>
        /// Converts the given array of bytes to a 16-bit signed integer.
        /// </summary>
        public short ToInt16(byte[] value, int startIndex) {
            return ToInt16(value, startIndex, mFrom, mTo);
        }

        /// <summary>
        /// Converts the given array of bytes to a 32-bit signed integer.
        /// </summary>
        public int ToInt32(byte[] value, int startIndex) {
            return ToInt32(value, startIndex, mFrom, mTo);
        }

        /// <summary>
        /// Converts the given array of bytes to a 64-bit signed integer.
        /// </summary>
        public long ToInt64(byte[] value, int startIndex) {
            return ToInt64(value, startIndex, mFrom, mTo);
        }

        /// <summary>
        /// Converts the given array of bytes to a single precision floating number.
        /// </summary>
        public float ToSingle(byte[] value, int startIndex) {
            return ToSingle(value, startIndex, mFrom, mTo);
        }

        /// <summary>
        /// Converts the given array of bytes to a double precision floating number.
        /// </summary>
        public double ToDouble(byte[] value, int startIndex) {
            return ToDouble(value, startIndex, mFrom, mTo);
        }

        /// <summary>
        /// Converts the given 16-bit unsigned integer to an array of bytes.
        /// </summary>
        public byte[] GetBytes(ushort value) {
            return GetBytes(value, mFrom, mTo);
        }

        /// <summary>
        /// Converts the given 32-bit unsigned integer to an array of bytes.
        /// </summary>
        public byte[] GetBytes(uint value) {
            return GetBytes(value, mFrom, mTo);
        }

        /// <summary>
        /// Converts the given 64-bit unsigned integer to an array of bytes.
        /// </summary>
        public byte[] GetBytes(ulong value) {
            return GetBytes(value, mFrom, mTo);
        }

        /// <summary>
        /// Converts the given 16-bit signed integer to an array of bytes.
        /// </summary>
        public byte[] GetBytes(short value) {
            return GetBytes(value, mFrom, mTo);
        }

        /// <summary>
        /// Converts the given 32-bit signed integer to an array of bytes.
        /// </summary>
        public byte[] GetBytes(int value) {
            return GetBytes(value, mFrom, mTo);
        }

        /// <summary>
        /// Converts the given 64-bit signed integer to an array of bytes.
        /// </summary>
        public byte[] GetBytes(long value) {
            return GetBytes(value, mFrom, mTo);
        }

        /// <summary>
        /// Converts the given single precision floating-point number to an array of bytes.
        /// </summary>
        public byte[] GetBytes(float value) {
            return GetBytes(value, mFrom, mTo);
        }

        /// <summary>
        /// Converts the given double precision floating-point number to an array of bytes.
        /// </summary>
        public byte[] GetBytes(double value) {
            return GetBytes(value, mFrom, mTo);
        }

        #endregion

        #region Private Helpers

        /// <summary>
        /// Reverse the array of bytes as needed.
        /// </summary>
        private static byte[] CheckData(byte[] value, int startIndex, int length, ByteOrder from, ByteOrder to) {
            from = CheckByteOrder(from);
            to = CheckByteOrder(to);
            byte[] data = new byte[length];
            Array.Copy(value, startIndex, data, 0, length);
            if (from != to) {
                Array.Reverse(data);
            }
            return data;
        }

        /// <summary>
        /// Reverse the array of bytes as needed.
        /// </summary>
        private static byte[] CheckData(byte[] value, ByteOrder from, ByteOrder to) {
            return CheckData(value, 0, value.Length, from, to);
        }

        /// <summary>
        /// Decodes the ByteOrder.System value for this platform.
        /// </summary>
        private static ByteOrder CheckByteOrder(ByteOrder order) {
            if (order == ByteOrder.System) {
                if (BitConverter.IsLittleEndian) {
                    return ByteOrder.LittleEndian;
                } else {
                    return ByteOrder.BigEndian;
                }
            } else {
                return order;
            }
        }

        #endregion
    }
}